Fork me on GitHub

单元测试学习笔记(一) 关于单元测试和Mockito

关于单元测试

单元测试的优点

  • 单元测试是指对软件中的最小可测试单元进行检查和验证。
  • 在Java中的单元测试,一般是对一个类的测试。能让coder极为迅速并且准确的定位错误的来源。因此,极大的减少了我们调试的时间。

关于隔离测试(Mock)和Mock对象

  • 在一个大项目或者关系比较紧密的项目中,很有可能出现两个子系统之间的接口依赖,因此极可能会造成前期开发中基础系统一直在开发接口,而自己的功能只能放后。
  • 隔离测试它使得我们可以测试还未写完的代码(只要你有接口可使用),另外,隔离测试能帮助团队单元测试代码的一部分,而无需等待全部代码的完成。
  • 如图:

    image

    从上图可以看出,如果我们要对A进行测试,那么就要先把整个依赖树构建出来,也就是BCDE的实例。

    而一种替代方案就是使用Mock:

    image

    • mock对象就是在调试期间用来作为真实对象的替代品。
    • mock测试就是在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法就叫mock测试。
    • Mockito就是一种mock框架。

关于测试替身

  • Stub(桩)-用简单可能的实现来代替真实的实现。
  • Fake(伪造对象)-优化的伪造真实事物的行为。
  • SPY(测试间谍)-用于记录发生的情况, 用于事后验证。
  • Mock(模拟对象)-特定情景下可配置行为的对象。

  • Mock和Stub的异同:

    • 相同点:Stub和Mock对象都是用来模拟外部依赖,使我们能控制。
    • 不同点:而stub完全是模拟一个外部依赖,用来提供测试时所需要的测试数据。而mock对象用来判断测试是否能通过,也就是用来验证测试中依赖对象间的交互能否达到预期。
    • 在mocking框架中mock对象可以同时作为stub和mock对象使用,两者并没有严格区别。

Mockito

什么是Mockito

  • Mockito是mocking框架,是Google Code上的一个开源项目,Api相对于EasyMock更好友好。Mockito简单易学,它可读性强和验证语法简洁。
  • 用Mockito完成单元测试的过程大致可以划分为以下几个步骤:
    1. 生成 Mock 对象。
    2. 设定 Mock 对象的预期行为和输出。
    3. 调用 Mock 对象方法进行单元测试。
    4. 对 Mock 对象的行为进行验证。

IDEA里新建某个类的测试类的方法

  • 需要下载Junit插件。
  • maven导入相关junit和mockito依赖包。
  • 建立或跳转到当前类的测试类:Ctrl+Shift+T。
  • 编辑测试和设置运行:
    • run-edit configuration 可以设置运行的测试类。
    • 下载junit插件后,类前有运行箭头,可选择run/debug/with coverage,最后一个是显示测试覆盖率。会在跑完后在被测试的类前显示颜色以标识覆盖情况(覆盖到的绿色,没覆盖到红色)。

Mocktio实例

以某段测试类代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//使用built-in runner:MockitoJUnitRunner初始化mock的代码
@RunWith(MockitoJUnitRunner.class)
public class AccNbrPrefixFormatTest {

//创建Mock对象模拟AccNbrService类
@Mock
AccNbrService accNbrService;

//自动注入Mock类(AccNbrService)到被测试类(AccNbrPrefixFormat),作为一个属性
@InjectMocks
AccNbrPrefixFormat accNbrPrefixFormat = new AccNbrPrefixFormat();

@Before
public void setUp() throws BaseAppException {
//实例化一个AccNbrPrefix类并赋值
AccNbrPrefix accNbrPrefix = new AccNbrPrefix();
accNbrPrefix.setAccNbr("190001");
accNbrPrefix.setPrefix("86");

String msisdnMock = "190001";
//通过 when(mock.someMethod()).thenReturn(value) 来设定 Mock 对象某个方法调用时的返回值。
//这里当调用模拟类的方法时,返回该实体类。
Mockito.when(accNbrService.formatPrefixAccNbr(msisdnMock)).thenReturn(accNbrPrefix);
}

@Test
public void execute() throws BaseAppException {
Map<String, Object> params = new HashMap<>();
params.put("MSISDN", "190001");

accNbrPrefixFormat.execute(params);
//断言:期望值与实际值对比
Assert.assertEquals("190001", params.get("ACC_NBR"));
Assert.assertEquals("86", params.get("PREFIX"));
}

}
几个常用注解
  • @RunWith(MockitoJUnitRunner.class):使用Mockito一般需要在类前加上这个注解。
  • @Mock:创建模拟的对象。
    • 注:不能mock静态、final、私有方法等。
  • @InjectMocks:被注入mock对象的被测试类。
  • @Test:把一个方法标记为测试方法。Test的属性有:

    • except:用来测试异常,如图,如果这个测试方法抛出了异常则通过,如果没抛出异常 则测试不通过执行fail(“factorial参数为负数没有抛出异常”)。

      image

    • timeout:测试一个方法能否在规定时间完成:
      如@Test(timeout=2000):测试该测试方法是否在2000毫秒内完成。

  • @Before:每一个测试方法执行前自动调用一次。
  • @After:每一个测试方法执行完自动调用一次。

  • @BeforeClass:所有测试方法执行前执行一次,在测试类还没有实例化就已经被加载,所以用static修饰。

  • @AfterClass:所有测试方法执行完执行一次,在测试类还没有实例化就已经被加载,所以用static修饰。
  • @Ignore:暂不执行该测试方法。

  • 执行顺序:BeforeClass→(构造方法)→Before→Test1→After→(构造方法)→Before→Test2→After→AfterClas。

    • @BeforeClass和@AfterClass在类被实例化前(构造方法执行前)就被调用了,而且只执行一次,通常用来初始化和关闭资源。
    • @Before和@After和在每个@Test执行前后都会被执行一次。
    • @Test标记一个方法为测试方法,被@Ignore标记的测试方法不会被执行,例如这个模块还没完成或者现在想测试别的不想测试这一块。
    • JUnit4为了保证每个测试方法都是单元测试,是独立的互不影响。所以每个测试方法执行前都会重新实例化测试类(构造方法多次调用)。

常用语法

设定 Mock 对象某个方法调用时的返回值

@Mock设置生成了mock对象,那么这是用Mockito完成单元测试的第二步:设定Mock对象的预期行为和输出。
有两种书写方式:

when(mock.someMethod()).thenReturn(value)

或者

doReturn(value).when(mock).someMethod()

验证被测试方法

即用Mockito完成单元测试的第四步:对Mock对象的行为进行验证。
Mock 对象一旦建立便会自动记录自己的交互行为,所以我们可以有选择的对它的 交互行为进行验证在 Mockito中验证 Mock 对象交互行为的方法是:

verify(mock).someMethod(…)。

可以验证调用次数,如:

verify(mock, times(num)).someMethod(…)。

1
2
3
4
5
6
7
@Test
public void customCheckAndFillUpWholesaleInstEx() throws BaseAppException {
WholesaleInstExDto wholesaleInstExDto = new WholesaleInstExDto();
transferAcctImportComp.customCheckAndFillUpWholesaleInstEx(wholesaleInstExDto);
// 是否校验非 bundle和VPN订户
Mockito.verify(wholesaleManager).checkSubsRela4WholesaleInst(wholesaleInstExDto);
}
对方法设定返回异常

when(list.get(1)).thenThrow(new RuntimeException(“test excpetion”));

或者

doThrow(new RuntimeException(“test excpetion”)).when(list).get(1);

doThrow在运行测试方法时如果报错会抛出相应异常,从而覆盖被测试方法中的catch部分。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  @InjectMocks
private BatchTransferAcctImportComp transferAcctImportComp;
@Mock
ExceptionChecker exceptionChecker;
@Mock
private AcctService acctService;
@Mock
private Acct acct;

@Test
public void buildWholesaleInstExWithWrongFormat() throws BaseAppException {
Mockito.doThrow(new BaseAppException(OrderErrorCodeDef.CVBS_ORDER_SUBS_CUST_IS_DIFFERENT)).when(exceptionChecker)
.publishBizException(OrderErrorCodeDef.CVBS_ORDER_SUBS_CUST_IS_DIFFERENT);

//创建用于测试的对象
WholesaleInstExDto wholesaleInstExDto = new WholesaleInstExDto();
wholesaleInstExDto.setAcctId("1");
wholesaleInstExDto.setCustId("1");
Map<String, String> paramStrMap = Maps.newHashMap();
paramStrMap.put("A", "1000");
//设置mock对象预期行为和输出
Mockito.when(acctService.queryAcctByAcctNbr("100")).thenReturn(acct);
Mockito.when(acct.getAcctId()).thenReturn("1");
Mockito.when(acct.getCustId()).thenReturn("2");
//调用被测试方法,这个方法校验wholesaleInstExDto和paramStrMap的CustId须一致。因此按照设定值,此处会抛出异常。
transferAcctImportComp.buildWholesaleInstEx(wholesaleInstExDto, paramStrMap);
}

也可以使用org.junit.rules.ExpectedException规则:

  • 标准的JUnit的org.junit.Test注解提供了一个expected属性,你可以用它来指定一个Throwble类型,如果方法调用中抛出了这个异常,这条测试用例就算通过了。很多情况下有它就足够了,不过如果你想验证下异常的信息——你就得另寻出路了。使用ExpectedException来实现这个非常简单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
      public class ExpectedExceptionsTest {
    @Rule
    public ExpectedException thrown = ExpectedException.none();
    @Test
    public void verifiesTypeAndMessage() {
    thrown.expect(RuntimeException.class);
    thrown.expectMessage("Runtime exception occurred");
    throw new RuntimeException("Runtime exception occurred");
    }
    }

    在这段代码中,我们可以期望抛出的异常中包含指定的信息。

参数匹配器
  • 如 anyInt、anyString、anyMap…..
  • 需要注意的是:如果使用参数匹配器,那么所有的参数都要使用参数匹配器,不管是stubbing还是verify的时候都一样。
    如:
    1
    2
    3
    4
    5
    6
    7
    	@Test  
    public void argumentMatcherTest2(){
    Map<Integer,String> map = mock(Map.class);
    when(map.put(anyInt(),anyString())).thenReturn("hello");//anyString()替换成"hello"就会报错
    map.put(1, "world");
    verify(map).put(eq(1), eq("world")); //eq("world")替换成"world"也会报错
    }
断言

即验证测试结果与期望值是否一致。
一些常用断言:

  • assertEquals
    比较实际的值和用户预期的值是否一样。
  • assertTrue、与assertFalse
    判断某个条件是真还是假,如果和预期的值相同则测试成功,否则将失败。
  • assertNull与assertNotNull
    验证所测试的对象是否为空或不为空。
  • assertSame与assertNotSame
    测试预期的值和实际的值是否为同一个参数(即判断是否为相同的引用)。assertNotSame则测试预期的值和实际的值是不为同一个参数。
  • fail
    fail断言能使测试立即失败,这种断言通常用于标记某个不应该被到达的分支。

to be continued